Skip to main content

8_1. References

A reference lets you access a value without owning it.

  • Written as &T or &mut T
  • Does not take ownership
  • Does not free memory
  • Must always be valid

References are Rust’s safe alternative to raw pointers.

Why References Exist​

Without references:

fn print(s: String) {
println!("{}", s);
}

let s = String::from("hello");
print(s);
// s is now gone

That’s annoying and inefficient.

With references:

fn print(s: &String) {
println!("{}", s);
}

let s = String::from("hello");
print(&s);
// s still exists

References enable safe sharing.

References and Ownership​

Ownership Rule​

References never own data.

let s = String::from("hello");
let r = &s;
  • s owns the heap allocation
  • r just points to it
  • When s is dropped, r becomes invalid (and Rust prevents this)

Immutable References (&T)​

What they allow

  • Read access
  • Multiple references at the same time
let s = String::from("hello");

let r1 = &s;
let r2 = &s;

println!("{} {}", r1, r2);

Why this is safe

  • No one can modify s
  • No data races
  • No invalid memory access

Mutable References (&mut T)​

What they allow

  • Read + write access
  • Exactly one mutable reference at a time
let mut s = String::from("hello");

let r = &mut s;
r.push_str(" world");

println!("{}", r);

Why this is restricted

  • Two writers = race conditions
  • Writer + reader = inconsistent reads

Rust blocks this at compile time.

Immutable vs Mutable Rule (Critical)​

At any moment, either:

  • Many immutable references
  • One mutable reference

Both at the same time

let r1 = &s;
let r2 = &mut s; // ❌ compile error

This rule is the foundation of Rust’s thread safety.

Reference Scope (Non-Lexical Lifetimes)​

Rust tracks actual usage, not just braces.

let mut s = String::from("hello");

let r1 = &s;
println!("{}", r1); // last use

let r2 = &mut s; // ✅ allowed
r2.push_str("!");

Even though r1 is still in scope textually, Rust knows it’s no longer used.

References in Function Parameters​

Immutable reference​

fn length(s: &String) -> usize {
s.len()
}

Mutable reference

fn append(s: &mut String) {
s.push('!');
}

Usage

let mut s = String::from("hello");

let len = length(&s);
append(&mut s);

println!("{} ({})", s, len);

References vs Copy​

References themselves are Copy:

let s = String::from("hello");

let r1 = &s;
let r2 = r1; // copy of reference
  • Both point to the same data
  • Borrowing rules still apply
  • Ownership does not change

Dangling References (Prevented)​

Rust forbids references that outlive data.

let r: &String;

{
let s = String::from("hello");
r = &s; // ❌ error
}

Why?

  • s is dropped
  • Heap memory freed
  • r would dangle

Rust stops this at compile time.

References and the Heap​

let s = String::from("hello");
let r = &s;

Memory layout:

Stack:
r ──→ s ──→ Heap("hello")
  • Reference points to stack value
  • Stack value points to heap
  • Lifetimes guarantee everything stays valid

References and Structs​

Struct Holding References​

struct Excerpt<'a> {
text: &'a str,
}

Excerpt cannot outlive the data it references

Usage:

let s = String::from("hello world");
let e = Excerpt { text: &s[0..5] };

println!("{}", e.text);

References and Pattern Matching​

Moving (bad)​

let opt = Some(String::from("hello"));

match opt {
Some(s) => println!("{}", s), // move
None => {}
}

Borrowing (good)​

match &opt {
Some(s) => println!("{}", s),
None => {}
}

Slices Are References​

let s = String::from("hello world");
let slice = &s[0..5];
  • &str is a reference
  • No allocation
  • No ownership transfer

Same for arrays:

let arr = [1, 2, 3];
let slice = &arr[..];

References vs Raw Pointers​

FeatureReferenceRaw Pointer
Always valid✅❌
Null allowed❌✅
Borrow checked✅❌
Safe by default✅❌

Raw pointers exist (*const T, *mut T) but require unsafe.

Mental Model That Works​

  • Ownership → who frees memory
  • References → who can access memory
  • Borrow checker → traffic cop
  • Lifetimes → how long access lasts

If the compiler allows it, the reference is guaranteed safe.